package libra.predicate.custom._1;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

import javax.validation.constraints.NotEmpty;

import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.CookieRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;

/**
 * 自定义Predicate断言
 * 注意事项：
 * 	1、自定义断言工厂必须是Spring组件，即IOC容器中的bean。
 * 	2、自定义断言工厂必须继承自抽象类AbstractRoutePredicateFactory。
 * 	3、自定义断言工厂命名必须是：xxxxRoutePredicateFactory，即以'RoutePredicateFactory'结尾。
 * 	   使用时，Gateway底层会根据'xxxx' + 'RoutePredicateFactory'，来进行反射查找对应的断言工厂。
 * 	   yml中配置项为'xxxx'
 * 	4、自定义断言工厂类中必须实现一个静态内部类 -> public static class Config{ xxx xxx }
 * 	   Config类的作用是用来接收yml配置文件中的配置信息。
 *     4.1、经过测试，该项并非必须项，也可以使用外部定义bean来完成Config。
 * 	5、自定义断言工厂类中必须实现shortcutFieldOrder()方法，来完成yml配置与Config类之间的值绑定。
 * 
 * shortcutFieldOrder() 与 Config 之间参数的绑定过程：
 * 	1、在yml中解析xxxxRoutePredicateFactory类名中的'xxxx'项，配置项如果有多个参数通过","分隔
 * 	2、通过xxxxRoutePredicateFactory.shortcutFieldOrder()方法中传入的属性名list，查找对应的set方法设置属性值
 * 	3、请求产生后，通过xxxxRoutePredicateFactory.apply()判断当前请求是否匹配
 * 		例：自定义断言工厂xxxxRoutePredicateFactory
 * 			   |-> 静态内部配置类Config{ 属性a、属性b、属性c }
 * 			yml配置：predicates.xxx=属性a【值】,属性b【值】,属性c【值】
 * 			xxxxRoutePredicateFactory.shortcutFieldOrder() -> [ 属性a【名】、属性b【名】、属性c【名】 ]
 * 			调用静态内部配置类Config内部的属性set方法，绑定参数值
 * 			xxxxRoutePredicateFactory.apply()断言当前请求的匹配结果
 * 			
 * @author libra
 */
@Component
public class SelfPredicateRoutePredicateFactory extends
	AbstractRoutePredicateFactory<SelfPredicateRoutePredicateFactory.Config> {
	
	// 构造函数需要将自身的静态内部Config类进行传入
	public SelfPredicateRoutePredicateFactory() {
		super(Config.class);
	}
	
	/**
	 * 通过该方法向Config类进行“值”设置
	 * 传入的值即为Config中的属性名，通过查找Config中的set方法进行设置“值”
	 */
	@Override
	public List<String> shortcutFieldOrder() {
		return Arrays.asList("method","cookie_Key","cookie_Val_Regex","param_Key","param_Val_Regex");
	}
	
	@Override
	public Predicate<ServerWebExchange> apply(Config config) {
		// 断言判定的方法实现，true->匹配成功；false->匹配失败；
		return new GatewayPredicate() {
			/**
			 * 当前测试用例：
			 * 	请求方式为post 并且 包含有请求参数 selfPredicate=纯数字的正则
			 */
			@Override
			public boolean test(ServerWebExchange serverWebExchange) {
				// 通过该方法获取当次这次的Request请求对象
				ServerHttpRequest request = serverWebExchange.getRequest();
				
				// 匹配请求类型，自定义实现
				if( !config.method.equalsIgnoreCase( request.getMethodValue() ) )
					return false;
				
				// 匹配请求cookie，采用CookieRoutePredicateFactory提供的默认实现
				Predicate<ServerWebExchange> cookiePredicate = 
					new CookieRoutePredicateFactory()
						.apply( new CookieRoutePredicateFactory.Config()
							.setName(config.cookie_Key).setRegexp(config.cookie_Val_Regex) );
				if ( !cookiePredicate.test(serverWebExchange) )
					return false;
				
				// 匹配请求参数，采用QueryRoutePredicateFactory提供的默认实现
				Predicate<ServerWebExchange> queryPredicate = 
					new QueryRoutePredicateFactory()
						.apply( new QueryRoutePredicateFactory.Config()
						.setParam(config.param_Key).setRegexp(config.param_Val_Regex) );
				
				/*
				 * !!! 当且仅当同时满足以下三个条件时，才会映射放行请求
				 * 条件1：请求方式必须为yml配置method的指定请求方式
				 * 条件2：请求携带有指定的cookie(key由yml配置的cookie_Key指定)，且cookie的值满足条件(val规则由yml配置的cookie_Val_Regex指定)
				 * 条件3：请求携带有指定的请求参数(key由yml配置的param_Key指定)，且参数值满足条件(val规则由yml配置的param_Val_Regex指定)
				 */
				return queryPredicate.test(serverWebExchange);
			}
		};
	}
	
	/**
	 * 内部静态Config配置类，用于存储yml中的配置值
	 * @author libra
	 */
	@Validated
	public static class Config {
		@NotEmpty
		private String method;
		@NotEmpty
		private String cookie_Key;
		@NotEmpty
		private String cookie_Val_Regex;
		@NotEmpty
		private String param_Key;
		@NotEmpty
		private String param_Val_Regex;
		public void setMethod(String method) {
			this.method = method;
		}
		public void setCookie_Key(String cookie_Key) {
			this.cookie_Key = cookie_Key;
		}
		public void setCookie_Val_Regex(String cookie_Val_Regex) {
			this.cookie_Val_Regex = cookie_Val_Regex;
		}
		public void setParam_Key(String param_Key) {
			this.param_Key = param_Key;
		}
		public void setParam_Val_Regex(String param_Val_Regex) {
			this.param_Val_Regex = param_Val_Regex;
		}
	}
}
